/*
 * Copyright 2004-2005 The Apache Software Foundation or its licensors,
 *                     as applicable.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.jackrabbit.classloader;

import java.util.ArrayList;
import java.util.Collection;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import javax.jcr.observation.ObservationManager;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The <code>DynamicPatternPath</code> class is a {@link PatternPath}
 * which registers for modifications in the repository which may affect the
 * result of calling the <code>getExpandedPaths</code> method. If also supports
 * for clients registering with instances of this class to be notified if such
 * an event happens.
 * <p>
 * To free the system from too much work, instances of this class are only
 * registered with the session's observation manager if at least one listener is
 * interested in notification to changes in the matched path list.
 *
 * @author Felix Meschberger
 * @version $Rev:$, $Date:$
 */
/* package */ class DynamicPatternPath extends PatternPath
        implements EventListener {

    /** default logger */
    private static final Log log =
        LogFactory.getLog(DynamicPatternPath.class);

    /** The list of registered listeners for this list */
    private final ArrayList listeners = new ArrayList();

    /**
     * <code>true</code> if this instance is registered with the session's
     * observation manager.
     */
    private boolean isRegistered;

    /**
     * Creates an instance of the <code>DynamicPatternPath</code> from
     * a collection of path patterns.
     *
     * @param session The session to access the Repository to expand the paths
     *      and to register as an event listener.
     * @param pathPatterns The array of path patterns to add.
     *
     * @throws NullPointerException if the <code>pathPatterns</code> array is
     *      <code>null</code>.
     *
     * @see PatternPath#PathPatternList(Session, String[])
     */
    /* package */ DynamicPatternPath(Session session, String[] pathPatterns) {
        super(session, pathPatterns);
    }

    //---------- notification listener registration and interface -------------

    /**
     * Adds the given listener to the list of registered listeners. If the
     * listener is already registered, it is not added a second time.
     * <p>
     * This is synchronized to prevent multiple parallel modification of the
     * listeners list by mutliple threads.
     *
     * @param listener The listener to register. This must not be
     *      <code>null</code>.
     *
     * @throws NullPointerException if the <code>listener</code> parameter is
     *      <code>null</code>.
     */
    /* package */ synchronized void addListener(Listener listener) {

        // check listener
        if (listener == null) {
            throw new NullPointerException("listener");
        }

        // make sure we get updated on changes to be able to notify listeners
        // we are pretty sure our listeners list will not be empty :-)
        if (!isRegistered) {
            log.debug("addListener: Register with observation service");
            registerEventListener();
        }

        // guarded add
        if (!listeners.contains(listener)) {
            log.debug("addListener: Listener " + listener);
            listeners.add(listener);
        } else {
            log.info("addListener: Listener " + listener + " already added");
        }
    }

    /**
     * Removes the given listener from the list of registered listeners. If the
     * listener is not registered, the list of registered listeners is not
     * modified.
     * <p>
     * This is synchronized to prevent multiple parallel modification of the
     * listeners list by mutliple threads.
     *
     * @param listener The listener to deregister. This must not be
     *      <code>null</code>.
     *
     * @throws NullPointerException if the <code>listener</code> parameter is
     *      <code>null</code>.
     */
    /* package */ synchronized void removeListener(Listener listener) {

        // check listener
        if (listener == null) {
            throw new NullPointerException("listener");
        }

        // guarded removal
        if (listeners.remove(listener)) {
            log.debug("removeListener: Listener " + listener);
        } else {
            log.info("removeListener: Listener " + listener +  " not registered");
        }

        // deregister if no listener is registered anymore
        // we are pretty sure to be registered
        if (listeners.size() == 0) {
            log.debug("removeListener: Deregister from observation service");
            unregisterEventListener();
        }
    }

    //---------- EventListener interface --------------------------------------

    /**
     * Handles the case where any change occurrs to the set of matched paths.
     * This is, if either a newly created item matches or a previously matching
     * item has been removed.
     * <p>
     * This method ignores <code>PROPERTY_CHANGED</code> events, as these
     * events do not have an influence on the set of matched paths.
     * <p>
     * The events in the iterator are analyzed until any non-property-change
     * event has an influence on the set of matched paths. As soon as such a
     * path is encountered, the listeners are notified and this method
     * terminates without further inspection of the events.
     *
     * @param events The iterator on the events being sent
     */
    public void onEvent(EventIterator events) {
        // check whether any of the events match the pattern list. If so
        // notify listeners on first match found and ignore rest for testing
        while (events.hasNext()) {
            Event event = events.nextEvent();

            // ignore property modifications
            if (event.getType() == Event.PROPERTY_CHANGED) {
                continue;
            }

            try {
                String path= event.getPath();
                if (matchPath(path)) {
                    log.debug("onEvent: Listener Notification due to " +
                        path);
                    notifyListeners();
                    return;
                }
            } catch (RepositoryException re) {
                log.info("onEvent: Cannot check events", re);
            }
        }
    }

    /**
     * Registers this list object with the session's observation manager to get
     * information on item updates.
     */
    private void registerEventListener() {

        // make sure we are not registered yet
        if (isRegistered) {
            log.debug("registerModificationListener: Already registered");
            return;
        }

        try {
            ObservationManager om =
                getSession().getWorkspace().getObservationManager();
            om.addEventListener(this, 0xffff, "/", true, null, null, false);
            isRegistered = true;
        } catch (RepositoryException re) {
            log.warn("registerModificationListener", re);
        }
    }

    /**
     * Unregisters this list object from the observation manager to not get
     * information on item updates anymore. This method is called when no more
     * listeners are interested on updates. This helps garbage collect this
     * object in the case no reference is held to the list anymore. If no one
     * is interested in changes anymore, we are not interested either, so we
     * may as well unregister.
     */
    private void unregisterEventListener() {

        // make sure we are registered
        if (!isRegistered) {
            log.debug("deregisterModificationListener: Not registered");
            return;
        }

        try {
            ObservationManager om =
                getSession().getWorkspace().getObservationManager();
            om.removeEventListener(this);
            isRegistered = false;
        } catch (RepositoryException re) {
            log.warn("deregisterModificationListener", re);
        }
    }

    /**
     * Notifies all registered listeners on the change in the set of matched
     * paths by calling their <code>pathListChanged</code> method.
     */
    private void notifyListeners() {
        for (int i=0; i < listeners.size(); i++) {
            Listener listener = (Listener) listeners.get(i);
            log.debug("notifyListeners: Notifying listener " + listener);
            try {
                listener.pathChanged();
            } catch (Exception e) {
                log.warn("notifyListeners: Listener " + listener + " threw: " + e);
                log.debug("dump", e);
            }
        }
    }

    /**
     * The <code>PatternPath.Listener</code> interface may be implemented
     * by interested classes to be notified as soon as the
     * {@link PatternPath#getExpandedPaths} method will return a
     * different result on the next invocation. This happens as soon as the set
     * of paths to which the list of patterns matches would change.
     */
    /* package */ interface Listener {

        /**
         * This method is called if the listener is to be notified of an event
         * resulting in the set of paths matched by the list of patterns to be
         * different.
         */
        public void pathChanged();
    }
}
